package fr.alma.domo.model;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import java.util.Collections;

/**
 * Classe utilisé pour gérer les evènements de modification de 
 * l' état des resources du système. Tous les services qui auront 
 * souhaités être notifié lors de la modification de la propriété
 * d'une resource, seront avertis s'ils ont déclaré une méthode 
 * portant l'annotation {@code fr.alma.domo.model.Watcher}
 * 
 * @author steg
 *
 */
@SuppressWarnings("serial")
public class EventHandler implements ResourceListener,Serializable{

	public final static EventHandler INSTANCE = new EventHandler();

	/**
	 * Encapsulation d'un listener
	 * 
	 * @author steg
	 *
	 */
	class Slot{
		public final Object target;
		public final Method method;
		public Slot(Object t,Method s){
			this.target = t;
			this.method = s;
		}
		/* (non-Javadoc)
		 * @see java.lang.Object#hashCode()
		 */
		@Override
		public int hashCode() {
			final int prime = 31;
			int result = 1;
			result = prime * result + getOuterType().hashCode();
			result = prime * result
					+ ((method == null) ? 0 : method.hashCode());
			result = prime * result
					+ ((target == null) ? 0 : target.hashCode());
			return result;
		}
		/* (non-Javadoc)
		 * @see java.lang.Object#equals(java.lang.Object)
		 */
		@Override
		public boolean equals(Object obj) {
			if (this == obj)
				return true;
			if (obj == null)
				return false;
			if (getClass() != obj.getClass())
				return false;
			Slot other = (Slot) obj;
			if (!getOuterType().equals(other.getOuterType()))
				return false;
			if (method == null) {
				if (other.method != null)
					return false;
			} else if (!method.equals(other.method))
				return false;
			if (target == null) {
				if (other.target != null)
					return false;
			} else if (!target.equals(other.target))
				return false;
			return true;
		}
		private EventHandler getOuterType() {
			return EventHandler.this;
		}
		
	}
	
	//[scope][property][target][slots]
	private static Map<String,Map<String,List<Slot>>> SLOTS 
		= Collections.synchronizedMap(new HashMap<String, Map<String,List<Slot>>>());
	//[scope][property][target][slots]
	private static Map<String,Map<String,List<Slot>>> STATIC_SLOTS 
		= Collections.synchronizedMap(new HashMap<String, Map<String,List<Slot>>>());

	/**
	 * Récupérer toutes les méthodes portant 
	 * l'annotation {@code fr.alma.domo.model.Watcher} et 
	 * les préparer pour notification.
	 * 
	 * @param target Un service
	 */
	public void connect(AbstractService target){					
		_loadWatcherFrom(target,target.getClass());
		for(Class<?> c : getClass().getInterfaces()){
			_loadWatcherFrom(target,c);
		}
		
		Class<?> cls = getClass().getSuperclass();
		while(cls!=null){
			_loadWatcherFrom(target,cls);
			cls=cls.getSuperclass();
		}		
	}
	
	/**
	 * Effacer toutes les réferences apportées par le service
	 * lors de l'appel de la méthode {@code fr.alma.domo.model.EventHandler#connect(AbstractService)}
	 * 
	 * @param target Le service
	 */
	public void disconnect(AbstractService target){
		_delSlot(target);
		_delStaticSlot(target);
	}
	
	/**
	 *  Permet d' écouter toutes les modfications sur des ressources
	 *  
	 *  @see fr.alma.domo.model.ResourceListener
	 */
	@Override
	public void onPropertyChange(Resource r,String property,Object oldValue,Object newValue){
		_dispatchOnStatic(r, property, oldValue, newValue);
		_dispatch(r, property, oldValue, newValue);
	}
	
	/**
	 * Récupérer toutes les méthode de <code>target</code> portant 
	 * l' annotation <code>fr.alma.domo.model.Watcher</code> et les 
	 * en registrer comme écouteur de modification sur des ressources
	 * @param target L'object cible
	 * @param clazz La class cible
	 */
	private final void _loadWatcherFrom(Object target,Class<?> clazz){
		for(Method m : clazz.getDeclaredMethods()){		
			if(m.isAnnotationPresent(Watcher.class)){
				Watcher w = m.getAnnotation(Watcher.class);
				_checkWatcher(w,m);
				if((m.getModifiers() & Modifier.STATIC)!=0){
					_addStaticScopeSlot(w.scope(), w.property(), clazz, m);
				}
				else{
					_addScopeSlot(w.scope(), w.property(), target, m);
				}
			}
		}
	}
	
	/**
	 * Verifier qu'une méthode correspond bien à la signature
	 * d' un observateur de ressource
	 * @param w
	 * @param m
	 * @throws <code>IllegalWatcherException</code> si l' observateur ne correspond pas
	 */
	private final void _checkWatcher(Watcher w,Method m){		
		Class<?>[] cls = m.getParameterTypes();
		if(cls.length!=4 
				|| cls[0]!=Resource.class
				|| cls[1]!=String.class
				|| cls[2]!=Object.class
				|| cls[3]!=Object.class)
		{
			throw new IllegalWatcherException("Bad watcher signature: A watcher must have the following parameters [Resource,String,Object,Object]");
		}
	}
	
	/**
	 * Enregistrer un observateur
	 * @param scope Le type de ressource qu'il souhaite observer
	 * @param property L' attribut qu'il souhaite observer
	 * @param target Le service déclarant l'observateur
	 * @param slot La méthode à appeler en cas d' évènement
	 */
	private final void _addScopeSlot(String scope,String property,Object target,Method slot){
		Map<String,List<Slot>> map = SLOTS.get(scope);
		if(map==null){
			map = Collections.synchronizedMap(new HashMap<String, List<Slot>>());		
		}
		
		Slot desc = new Slot(target,slot);
		List<Slot> list = map.get(property);
		if(list==null){
			list = Collections.synchronizedList(new ArrayList<Slot>());
		}
		if(!list.contains(desc)){
			list.add(desc);
		}
		
		map.put(property, list);
		SLOTS.put(scope, map);
	}

	/**
	 * Enregistrer un observateur static
	 * @param scope Le type de ressource qu'il souhaite observer
	 * @param property L' attribut qu'il souhaite observer
	 * @param target Le service déclarant l'observateur
	 * @param slot La méthode à appeler en cas d' évènement
	 */
	private final void _addStaticScopeSlot(String scope,String property,Class<?> target,Method slot){
		Map<String,List<Slot>> map = STATIC_SLOTS.get(scope);
		if(map==null){
			map = Collections.synchronizedMap(new HashMap<String, List<Slot>>());		
		}
		
		Slot desc = new Slot(target,slot);
		List<Slot> list = map.get(property);
		if(list==null){
			list = Collections.synchronizedList(new ArrayList<Slot>());
		}
		if(!list.contains(desc)){
			list.add(desc);
		}
		
		map.put(property, list);
		STATIC_SLOTS.put(scope, map);
	}
	
	/**
	 * Effacer tous les observateurs déclarés par un objet
	 * @param target L' objet
	 */
	private final void _delSlot(Object target){
		synchronized(SLOTS){
			for(Map<String,List<Slot>> m : SLOTS.values()){
				synchronized(m){
					for(List<Slot> l : m.values()){
						synchronized(l){
							List<Slot> toRemove = new ArrayList<Slot>();
							for(Slot d : l){
								if(d.target.equals(target)){
									toRemove.add(d);
								}
							}
							l.removeAll(toRemove);
						}
					}
				}
			}
		}
	}
	
	/**
	 * Efface un observateur static du registre
	 * @param target
	 */
	private final void _delStaticSlot(Object target){
		synchronized(STATIC_SLOTS){
			for(Map<String,List<Slot>> m : STATIC_SLOTS.values()){
				synchronized(m){
					for(List<Slot> l : m.values()){
						synchronized(l){
							List<Slot> toRemove = new ArrayList<Slot>();
							for(Slot d : l){
								if(d.target.equals(target.getClass())){
									toRemove.add(d);
								}
							}
							l.removeAll(toRemove);
						}
					}
				}
			}
		}
	}
	
	/**
	 * Notifie d'une modification d'une ressource à tous les 
	 * observateurs déclarés et la prenant en charge
	 * @param r La ressource
	 * @param property L' attribut qui à été modifié
	 * @param oldValue L' ancienne valeur
	 * @param newValue La nouvelle valeur
	 */
	private final void _dispatch(Resource r,String property,Object oldValue,Object newValue){
		synchronized(SLOTS){
			Map<String,List<Slot>> m = SLOTS.get(r.getAttribute(Resource.SCOPE, String.class));
			if(m!=null){
				synchronized(m){
					List<Slot> l = m.get(property);
					if(l!=null){
						synchronized(l){
							for(Slot d : l){
								try{
									d.method.setAccessible(true);
									d.method.invoke(d.target,r,property,oldValue,newValue);
								} catch(InvocationTargetException t){
									t.getTargetException().printStackTrace();
								} catch(Throwable t){
									t.printStackTrace();
								}
							}
						}
					}
				}
			}
		}		
	}

	/**
	 * Notifie d'une modification d'une ressource à tous les 
	 * observateurs déclarés static et la prenant en charge
	 * @param r La ressource
	 * @param property L' attribut qui à été modifié
	 * @param oldValue L' ancienne valeur
	 * @param newValue La nouvelle valeur
	 */
	private final void _dispatchOnStatic(Resource r,String property,Object oldValue,Object newValue){
		synchronized(STATIC_SLOTS){
			//on recupere les observateurs sur le type de r
			Map<String,List<Slot>> m = STATIC_SLOTS.get(r.getAttribute(Resource.SCOPE, String.class));
			if(m!=null){
				synchronized(m){
					List<Slot> l = m.get(property);
					if(l!=null){
						synchronized(l){
							for(Slot d : l){
								try{
									d.method.setAccessible(true);
									d.method.invoke(null,r,property,oldValue,newValue);
								} catch(InvocationTargetException t){
									t.getTargetException().printStackTrace();
								} catch(Throwable t){
									t.printStackTrace();
								}
							}
						}
					}
				}
			}
		}		
	}
	
	
}
